package com.ego.search.service.impl;

import com.ego.common.utils.JsonUtils;
import com.ego.common.utils.NumberUtils;
import com.ego.item.bo.SpuBo;
import com.ego.item.pojo.Brand;
import com.ego.item.pojo.Category;
import com.ego.search.bo.SearchRequest;
import com.ego.search.bo.SearchResponse;
import com.ego.search.feign.BrandFeignClient;
import com.ego.search.feign.CategoryFeignClient;
import com.ego.search.feign.GoodsFeignClient;
import com.ego.search.feign.SpecFeignClient;
import com.ego.search.pojo.Goods;
import com.ego.search.repository.GoodsRepository;
import com.ego.search.service.SearchService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.histogram.ParsedHistogram;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.metrics.ParsedStats;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SearchServiceImpl implements SearchService {

    @Resource
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Resource
    private CategoryFeignClient categoryFeignClient;

    @Resource
    private BrandFeignClient brandFeignClient;

    @Resource
    private SpecFeignClient specFeignClient;

    @Resource
    private GoodsFeignClient goodsFeignClient;

    @Resource
    private GoodsRepository goodsRepository;

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void createIndex(Long id) {
        SpuBo spuBo = goodsFeignClient.queryGoodsById(id);
        // 构建商品
        Goods goods = buildGoods(spuBo);
        // 保存数据到索引库
        goodsRepository.save(goods);
    }

    @Override
    public void deleteIndex(Long id) {
        goodsRepository.deleteById(id);
    }

    @Override
    public Goods buildGoods(SpuBo spuBo) {
        Goods result = null;
        try {
            //将List转成json
            String skus = objectMapper.writeValueAsString(spuBo.getSkus());
            //解析规格参数，将可搜索的规格参数存入specs中
            Map<String, Object> specs = new HashMap<>();
            //获取到字符串 规格参数
            String specifications = spuBo.getSpuDetail().getSpecifications();
            //将 字符串(specifications) 规格参数 -> 对象(specs)
            List<Map<String,Object>> specList = objectMapper.readValue(specifications, new TypeReference<List<Map<String, Object>>>() {});
            //遍历specList，解析需要搜索的规格参数
            specList.forEach(map->{
                List<Map<String,Object>> params = (List<Map<String, Object>>) map.get("params");
                params.forEach(param->{
                    //只要可搜索的规格参数
                    if ((Boolean) param.get("searchable")) {
                        Object v = param.get("v");
                        //有v就存v，没有就存options
                        if (v != null) {
                            specs.put(param.get("k").toString(), v);
                        }
                        else {
                            specs.put(param.get("k").toString(), param.get("options"));
                        }
                    }
                });
            });

            result = Goods.builder()
                    .id(spuBo.getId())
                    .subTitle(spuBo.getSubTitle())
                    .all(spuBo.getTitle()+""+spuBo.getCategoryNames()+""+spuBo.getBrandName())
                    .brandId(spuBo.getBrandId())
                    .cid1(spuBo.getCid1())
                    .cid2(spuBo.getCid2())
                    .cid3(spuBo.getCid3())
                    .createTime(spuBo.getCreateTime())
                    .price(spuBo.getSkus().stream().map(sku -> sku.getPrice()).collect(Collectors.toList()))
                    .skus(skus)
                    .specs(specs).build();
        } catch (JsonProcessingException e) {
            log.error("SpuBo{}转换数据异常",spuBo.getId());
        }
        return result;
    }

    @Override
    public SearchResponse search(SearchRequest searchRequest) {
        SearchResponse result = new SearchResponse();

        String key = searchRequest.getKey();
        // 判断是否有搜索条件，如果没有，直接返回null。不允许搜索全部商品
        if (StringUtils.isEmpty(key)) {
            return null;
        }

        // 构建查询条件
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"}, null));
        // 对key进行全文检索查询
        QueryBuilder basicQuery = buildBaiscQuery(searchRequest);
        nativeSearchQueryBuilder.withQuery(basicQuery);
        //分页
        Integer page = searchRequest.getPage();
        Integer size = searchRequest.getSize();
        nativeSearchQueryBuilder.withPageable(PageRequest.of(page-1,size));

        //添加固定字段聚合(cid3,brandId)词条聚合
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("categoryAgg").field("cid3"));
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("brandAgg").field("brandId"));

        //获取结果
        SearchHits<Goods> searchHits = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), Goods.class);

        //解析固定字段聚合结果
        List<Category> categoryList = getCategoryList(searchHits);
        List<Brand> brandList = getBrandList(searchHits);

        result.setCategoryList(categoryList);
        result.setBrandList(brandList);

        if (CollectionUtils.isNotEmpty(categoryList) && categoryList.size() > 0) {
            List<Map<String,Object>> specsList = getSpecsList(basicQuery,categoryList.get(0));
            result.setSpecsList(specsList);
        }

        long total = searchHits.getTotalHits();//总条数
        long totalPage = total%size==0 ? total/size:total/size+1;//总页数

        result.setTotal(total);
        result.setTotalPage(totalPage);
        result.setItems(searchHits.getSearchHits().stream().map(goodsSearchHit -> goodsSearchHit.getContent()).collect(Collectors.toList()));

        return result;
    }

    /**
     * 构建带过滤条件的基本查询
     * @param searchRequest 查询条件
     * @return
     */
    private QueryBuilder buildBaiscQuery(SearchRequest searchRequest) {
        //构建一个布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //添加match查询
        boolQueryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()));
        Map<String,String> filter = searchRequest.getFilters();
        filter.forEach((key,value) -> {
            Pattern pattern = Pattern.compile("^(\\d+.?\\d?)[-](\\d+.?\\d?)$");
            Matcher matcher = pattern.matcher(value);
            if (!matcher.find()) {
                //字符型采用词条过滤
                String name = null;
                if ("brandId".equals(key) || "cid3".equals(key)) {
                    name = key;
                }else {
                    name = "specs."+key+".keyword";
                }
                boolQueryBuilder.filter(QueryBuilders.termQuery(name,value));
            }else {
                //数字型采用范围过滤
                String begin = matcher.group(1);
                String end = matcher.group(2);
                boolQueryBuilder.filter(QueryBuilders.rangeQuery("specs."+key).gte(begin).lt(end));
            }
        });
        return boolQueryBuilder;
    }

    /**
     * 获取其他过滤条件
     * @param basicQuery
     * @param category
     * @return
     */
    private List<Map<String, Object>> getSpecsList(QueryBuilder basicQuery, Category category) {
        List<Map<String, Object>> result = new ArrayList<>();
        //1.查询出第一个类别的规格参数
        String specifications = specFeignClient.querySpecByCid(category.getId()).getBody();
        //2.区分出字符型和数字型规格参数
        Set<String> strSpecs = new HashSet<>();
        Map<String,String> numSpecs = new HashMap<>();
        List<Map<String, Object>> groupList = JsonUtils.nativeRead(specifications, new TypeReference<List<Map<String, Object>>>() {});
        groupList.forEach(group->{
            List<Map<String,Object>> params =  (List<Map<String,Object>>)group.get("params");
            params.forEach(param->{
                Boolean numerical = (Boolean)param.get("numerical");
                Boolean searchable = (Boolean)param.get("searchable");
                String k = param.get("k").toString();
                //判断是否可搜索
                if (searchable == null || !searchable) {
                    return;
                }
                //判断是否为数字型
                if (numerical != null && numerical) {
                    //如果为数字型，需要获取单位unit
                    numSpecs.put(k, param.get("unit").toString());
                } else {
                    //如果为字符型，只要k
                    strSpecs.add(k);
                }
            });
        });
        //3.字符型采用词条聚合
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withQuery(basicQuery);
        strSpecs.forEach(spec->{
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(spec).field("specs." + spec + ".keyword"));
        });

        //4.计算出每个数字型规格参数的间隔
        Map<String, Double> numSpecIntervalMap = getNumSpecIntervalMap(basicQuery,numSpecs);
        //5.数字型采用阶梯聚合
        numSpecs.keySet().forEach(spec->{
            Double interval = numSpecIntervalMap.get(spec);
            if (interval > 0) {//间隔大于零才添加
                nativeSearchQueryBuilder.addAggregation(AggregationBuilders.histogram(spec).field("specs."+spec).interval(interval));
            }
        });
        //6.执行聚合查询
        SearchHits<Goods> searchHits = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), Goods.class);

        //7.解析聚合结果
        //7.1.解析字符型聚合结果
        strSpecs.forEach(spec->{
            ParsedStringTerms aggregation = searchHits.getAggregations().get(spec);

            Map<String, Object> filter = new HashMap<>();
            filter.put("name", spec);
            //将每个bucket转成前端的option对象，并且option放进一个新的list
            List<Map<String, String>> options = aggregation.getBuckets().stream()
                    .map(bucket -> {
                        Map<String, String> option = new HashMap<>();
                        String key = bucket.getKeyAsString();
                        option.put("name", key);
                        return option;
                    }).filter(option -> StringUtils.isNotEmpty(option.get("name")))//不为空的才收集起来
                    .collect(Collectors.toList());
            filter.put("options", options);
            if (options.size() > 0) {
                //大于0，才把添加参数，即才显示在页面
                result.add(filter);
            }
        });
        //7.2.解析数字型聚合结果
        numSpecs.forEach((spec,unit)->{
            ParsedHistogram aggregation = searchHits.getAggregations().get(spec);
            if (aggregation != null) {//不为空才解析
                List<Map<String, String>> options = aggregation.getBuckets().stream()
                        .map(bucket -> {
                            Map<String, String> option = new HashMap<>();
                            Double begin = (Double) bucket.getKey();
                            Double end = begin+numSpecIntervalMap.get(spec);
                            String name = null;
                            //判断是否是整型，如果是就不要小数点
                            if (begin.intValue() == begin && end.intValue() == end) {
                                name = begin.intValue() + "-" + end.intValue();
                            } else {
                                name = NumberUtils.scale(begin,1) + "-" + NumberUtils.scale(end,1);
                            }
                            option.put("name", name);
                            return option;
                        }).collect(Collectors.toList());

                Map<String, Object> filter = new HashMap<>();
                filter.put("name",spec);
                filter.put("options",options);
                filter.put("unit",unit);
                result.add(filter);
            }
        });
        return result;
    }

    /**
     * 聚合出所有数字型参数间隔
     * @param basicQuery
     * @param numSpecs
     * @return
     */
    private Map<String, Double> getNumSpecIntervalMap(QueryBuilder basicQuery, Map<String, String> numSpecs) {
        Map<String, Double> result = new HashMap<>();

        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withQuery(basicQuery);
        //添加聚合条件
        numSpecs.keySet().forEach(spec->{
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.stats(spec).field("specs." + spec));
        });
        //执行聚合查询
        SearchHits<Goods> searchHits = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), Goods.class);
        //解析聚合结果
        numSpecs.keySet().forEach(spec->{
            ParsedStats aggregation = searchHits.getAggregations().get(spec);
            double interval = NumberUtils.getInterval(aggregation.getMin(), aggregation.getMax(), aggregation.getSum());
            result.put(spec, interval);
        });
        return result;
    }

    /**
     * 获取分类聚合结果
     * @param searchHits searchHits
     * @return Category集合
     */
    private List<Category> getCategoryList(SearchHits<Goods> searchHits) {
        ParsedLongTerms categoryAgg = searchHits.getAggregations().get("categoryAgg");
        List<Long> categoryIdList = categoryAgg.getBuckets().stream()
                .map(bucket -> bucket.getKeyAsNumber().longValue())
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(categoryIdList)) {
            return null;
        }
        return categoryFeignClient.queryListByIdList(categoryIdList).getBody();
    }

    /**
     * 获取品牌聚合结果
     * @param searchHits searchHits
     * @return Brand集合
     */
    private List<Brand> getBrandList(SearchHits<Goods> searchHits) {
        ParsedLongTerms brandAgg = searchHits.getAggregations().get("brandAgg");
        List<Long> brandIdList = brandAgg.getBuckets().stream()
                .map(bucket -> bucket.getKeyAsNumber().longValue())
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(brandIdList)) {
            return null;
        }
        return brandFeignClient.queryListByIdList(brandIdList).getBody();
    }
}
